from __future__ import with_statement
import os
import sys
import logging
import subprocess
import shutil
import time
import hashlib
from optparse import OptionParser

from browsertests.utils import cli_wrapper
from browsertests.tests.models import Status

log = logging.getLogger(__name__)

## XXX
if sys.platform == "win32":
    HG = r"c:\progfilesx86\Mercurial\hg.exe"
else:
    HG = "hg"

def get_hg_id(checkout_dir):
    hgid_output = subprocess.Popen([HG, "id"], cwd=checkout_dir,
                                   stdout=subprocess.PIPE).communicate()[0]
    log.debug("hg id output %s", hgid_output.strip())
    hgid = hgid_output.split()[0]
    return hgid

def hg_checkout(repourl, parent_dir, basename=None):
    assert not repourl.endswith("/"), "Trailing slash not allowed"
    if not basename:
        basename = os.path.basename(repourl)
    log.info("Checking out hg repo '%s' to %s/%s", repourl, parent_dir,
             basename)
    log.debug("basename: %s", basename)
    checkout_dir = os.path.join(parent_dir, basename)
    if os.path.isdir(checkout_dir):
        hgid = get_hg_id(checkout_dir)
        if hgid.endswith("+"):
            log.warn("checkout directory '%s' has local modifications, will "
                     "be removed" % checkout_dir)
            # XXX stop for debugging
            raise Exception("checkout directory '%s' has local modifications, will "
                            "be removed" % checkout_dir)
            shutil.rmtree(checkout_dir)

    if os.path.isdir(checkout_dir):
        log.debug("Pulling from repo %s", repourl)
        subprocess.check_call([HG, "-q", "pull", "-u"], cwd=checkout_dir)
    else:
        log.debug("Cloning repo %s", repourl)
        subprocess.check_call([HG, "-q", "clone", repourl, basename],
                              cwd=parent_dir)


class TestsSource(object):
    def __init__(self, description, repourl):
        self._description = description
        assert not repourl.endswith("/"), "Don't use a trailing slash on repourl"
        self._repourl = repourl
        self._workdir = None

    def set_workdir(self, workdir):
        self._workdir = workdir

    def get_description(self):
        return self._description

    def get_repourl(self):
        return self._repourl

    def get_version(self):
        return "<no version info available>"

    def get_dest(self):
        return "<no dest info available>"

    def update(self):
        pass

    def copy_to_tests(self, tests_dir):
        pass

    def get_externals_message(self):
        return (" * %(description)s\n"
                "   * repository: %(repourl)s\n"
                "   * at version: %(version)s\n"
                "   * installed into: %(dest)s\n\n" % {
                    "description": self.get_description(),
                    "repourl": self.get_repourl(),
                    "version": self.get_version(),
                    "dest": self.get_dest(),
                })

    def __str__(self):
        return "<%s repourl:%s>" % (self.__class__.__name__, self._repourl)

class MozillaMochitests(TestsSource):
    def __init__(self, repourl, objdir):
        super(MozillaMochitests, self).__init__("Mozilla Mochitests", repourl)
        self._objdir = objdir
        self._dests = []

    def copy_to_tests(self, tests_dir):
        copy_dirs = [
            ("_tests/testing/mochitest/tests", "tests"),
            ("_tests/testing/mochitest/MochiKit", "MochiKit"),
            ("_tests/testing/mochitest/static", "static"),
            # XXX copy that too?
            #("_tests/testing/mochitest/chrome", "chrome"),
        ]
        for source_copy_dir, target_copy_dir in copy_dirs:
            source_dir = os.path.join(self._objdir, source_copy_dir)
            target_dir = os.path.join(tests_dir, target_copy_dir)
            self._dests.append(target_copy_dir)
            log.debug("Copying from '%s' to '%s'", source_dir, target_dir)
            shutil.copytree(source_dir, target_dir)

    def get_version(self):
        inipath = os.path.join(self._objdir, "dist", "bin", "application.ini")
        assert os.path.isfile(inipath), "Can't find application.ini (%s)" % inipath
        source_stamp = None
        with open(inipath) as f:
            for line in f:
                line = line.strip()
                if line.startswith('SourceStamp'):
                    source_stamp = line.split("=")[1]
        assert source_stamp, "Couldn't find SourceStamp line in application.ini"
        return source_stamp

    def get_dest(self):
        return ",".join(self._dests)


class SCMSource(TestsSource):
    def __init__(self, description, repourl, dest="", excludes=None):
        super(SCMSource, self).__init__(description, repourl)
        assert not "\\" in dest, "Do not use backslash in dest (%s)" % dest
        self._dest = dest
        self._excludes = excludes
        if self._excludes == None:
            self._excludes = []

    def set_workdir(self, workdir):
        super(SCMSource, self).set_workdir(workdir)
        # _checkout_basename contains a hash of the repourl to prevent
        # collisions and avoid issues if the repourl changes.
        self._checkout_basename = "%s_%s" % (os.path.basename(self._repourl),
                                        hashlib.md5(self._repourl).hexdigest())
        self._checkout_dir = os.path.join(self._workdir, self._checkout_basename)

    def copy_to_tests(self, tests_dir):
        dest_dir = "tests"
        if self._dest:
            dest_dir += "/" + self._dest
        target_dir = os.path.join(self._workdir, dest_dir)
        if not os.path.isdir(target_dir):
            os.makedirs(target_dir)

        rsync_cmd = ["rsync", "-a", "--exclude=.svn", "--exclude=.hg"]
        for exclude in self._excludes:
            rsync_cmd.append("--exclude=" + exclude)
        # Do not delete if dest is empty
        if self._dest:
            rsync_cmd.extend(["--delete", "--delete-excluded"])
        log.debug("rsync command line: %s", rsync_cmd)
        rsync_cmd.extend([self._checkout_basename + "/", dest_dir])

        subprocess.check_call(rsync_cmd, cwd=self._workdir)

    def get_dest(self):
        return (self._dest if self._dest else ".")

class SVNSource(SCMSource):
    def update(self):
        subprocess.check_call(
            ["svn", "checkout", "-q", self._repourl, self._checkout_basename],
            cwd=self._workdir
        )

    def get_version(self):
        version = subprocess.Popen(["svnversion"], cwd=self._checkout_dir,
                                   stdout=subprocess.PIPE).communicate()[0]
        version = version.strip()
        if "M" in version:
            raise Exception("SVN directory %s has local modifications" %
                            self._checkout_dir)
        log.debug("svn version: %s", version)
        return version

class MercurialSource(SCMSource):
    def update(self):
        hg_checkout(self._repourl, self._workdir, self._checkout_basename)

    def get_version(self):
        # TODO optimization: hg id may already have been computed in hg_checkout.
        hgid = get_hg_id(self._checkout_dir)
        if hgid.endswith("+"):
            raise Exception("hg directory %s has local modifications" %
                            self._checkout_dir)
        log.debug("hg version: %s", hgid)
        return hgid

class MozillaReftests(MercurialSource):
    def __init__(self, repourl, dest=""):
        super(MozillaReftests, self).__init__("Mozilla Reftests", repourl,
                                              dest)

    def copy_to_tests(self, tests_dir):
        # Copy only directories (and their children) if they contain a
        # reftest.list manifest
        copy_dirs = []
        for root, dirs, files in os.walk(self._checkout_dir):
            for exclude in (".svn", ".hg", "CVS"):
                if exclude in dirs:
                    dirs.remove(exclude)
            if "reftest.list" in files:
                copy_dirs.append(root[len(self._checkout_dir) + 1:])
                dirs[:] = []

        for copy_dir in copy_dirs:
            source_dir = os.path.join(self._checkout_dir, copy_dir)
            target_dir = os.path.join(tests_dir, self._dest, copy_dir)
            log.debug("Copying from '%s' -> '%s'", source_dir, target_dir)
            shutil.copytree(source_dir, target_dir)

def tests_updater(tests_config_path, workdir, update_status=False,
                  dummy_mode=False):
    assert os.path.isdir(workdir), "Workdir %s does not exist" % workdir
    assert os.path.isfile(tests_config_path), ("Tests config file %s does not "
                                               "exist" % tests_config_path)

    loc = {}
    execfile(tests_config_path, {}, loc)
    assert "sources" in loc, ("Configuration '%s' did not set a 'sources' "
                               "list" % tests_config_path)
    assert "TESTS_REPO_URL" in loc, ("Configuration '%s' did not set a "
                                     "'TESTS_REPO_URL' variable" %
                                     tests_config_path)
    tests_repo_url = loc["TESTS_REPO_URL"]

    # TODO: check for TESTS_REPO_TYPE, and do not assume mercurial

    tests_dir = os.path.join(workdir, "tests")
    hg_checkout(tests_repo_url, workdir, "tests")

    # Remove everything except .hg and directories that will be rsynced.
    log.info("Clearing tests repo before external import.")
    keep_dirs = []
    for ts in loc["sources"]:
        if not type(ts) in (MercurialSource, SVNSource):
            continue
        if not ts._dest:
            continue
        keep_dirs.append(ts._dest.split("/")[0])
    log.debug("keep_dirs: %s", keep_dirs)

    for entry in os.listdir(tests_dir):
        if entry == ".hg":
            continue
        if entry in keep_dirs:
            log.debug("Keeping directory %s", entry)
            continue
        path = os.path.join(tests_dir, entry)
        if os.path.isfile(path):
            os.unlink(path)
        else:
            shutil.rmtree(path)

    for ts in loc["sources"]:
        ts.set_workdir(workdir)
        log.info("Updating %s..." % ts)
        ts.update()
        log.info("Copying files to tests directory for %s" % ts)
        ts.copy_to_tests(tests_dir)

    externals_content = "Externally imported tests report\n"
    externals_content += "================================\n\n"
    externals_content += " imported at %s\n\n" % time.strftime("%Y-%m-%d %H:%M")

    for ts in loc["sources"]:
        externals_content += ts.get_externals_message()

    with open(os.path.join(tests_dir, "externals.txt"), "wb") as f:
        f.write(externals_content)

    if dummy_mode:
        log.info("Dummy mode, not committing nor pushing tests repo")
    else:
        log.debug("Running hg addremove")
        subprocess.check_call([HG, "-q", "addremove"], cwd=tests_dir)
        log.debug("Committing changes")
        subprocess.check_call([HG, "-q", "ci", "-m", "automated tests update"],
                              cwd=tests_dir)
        log.debug("Pushing changes")
        subprocess.check_call([HG, "-q", "push"], cwd=tests_dir)
    if update_status:
        status = Status.objects.get(pk=1)
        if status.generation == Status.BUSY:
            log.warn("Status in busy state, another import in progress?")
            sys.exit(3)
            return
        status.generation += 1
        status.save()
        log.debug("new generation: %s", status.generation)

def main():
    usage = "%prog [OPTIONS] TESTS_CONFIG_FILE WORKDIR"
    parser = OptionParser(usage=usage)
    parser.add_option("--update-status", action="store_true", default=False,
        help="Update the status table to stop pending tests during browser "
             "update")

    # TODO: add a clobber option to purge the workdir.

    parser = cli_wrapper.add_common_parser_options(parser)
    options, args = parser.parse_args()

    if len(args) != 2:
        parser.print_help()
        sys.exit(2)

    tests_config_path, workdir = args
    cli_wrapper.process_options(options)

    tests_updater(tests_config_path, workdir, options.update_status,
                  options.dummy)

if __name__ == "__main__":
    main()
